Skip to content

chore(release): automate GitHub Release + document the flow#50

Merged
KylinMountain merged 7 commits into
mainfrom
chore/release-process
May 15, 2026
Merged

chore(release): automate GitHub Release + document the flow#50
KylinMountain merged 7 commits into
mainfrom
chore/release-process

Conversation

@KylinMountain
Copy link
Copy Markdown
Collaborator

Summary

`.github/workflows/publish.yml` has been in the repo for a while but no release has actually gone through it — all 6 PyPI versions (0.0.1 through 0.1.3) were uploaded directly via `twine` from a maintainer's laptop. There are zero git tags on the remote, the GitHub Releases page is empty, and the project has no changelog. This PR closes the gap so the next release can flow tag → CI → PyPI → GitHub Release in one step.

Changes

`.github/workflows/publish.yml`

  • Added a pre-build step that asserts the pushed tag matches `pyproject.toml` `[project].version` exactly (e.g. `v0.1.4` ↔ `0.1.4`). This catches the most common release mistake before any artifact is built.
  • Added a final step using `softprops/action-gh-release@v3.0.0` (pinned by SHA, same style as the other actions in the file) to create a GitHub Release on each `v*` tag with `generate_release_notes: true` and the built `dist/*` attached.
  • Granted `contents: write` permission to the job — required for the release-creation step. `id-token: write` (used by PyPI OIDC) is unchanged.

`RELEASING.md` (new)
Maintainer-facing doc. The flow is now strictly:

  1. Bump version in `pyproject.toml`.
  2. Move `[Unreleased]` entries in `CHANGELOG.md` into a new dated section.
  3. Commit and push to `main`.
  4. `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin vX.Y.Z`.
  5. CI does the rest.

Explicitly calls out: do not run `twine upload` locally anymore.

`CHANGELOG.md` (new)
Keep-a-Changelog skeleton with an `[Unreleased]` section listing the substantive changes already merged or in flight (#34, #38, #45, #47 / #49, and #48 if it lands). The four pre-existing PyPI releases (0.1.0, 0.1.1, 0.1.3 — 0.1.2 leaves no commit trail and only exists on PyPI) are backfilled with short summaries from `git log`, honestly noting that no tag was created at the time.

What this PR deliberately does NOT do

  • Retroactively tag 0.1.3. Doing so would push `v0.1.3` and trigger the workflow to attempt to re-publish 0.1.3, which PyPI would reject. So the first real exercise of the new flow will be the next version bump (0.1.4 or whatever).
  • Switch to setuptools-scm / dynamic versioning. That's a bigger refactor and out of scope here. The version-match assertion in CI is the lightweight equivalent.

Test plan

  • `yamllint` clean (no whitespace issues).
  • Reviewer to verify on a dry-run: push a `v0.0.0-test` tag from a fork or a deletable branch and confirm:
    • The tag-version assertion fires correctly on mismatch.
    • The PyPI publish step runs (will fail on duplicate version, which is fine for dry-run).
    • The release-creation step would have run.
  • The next real release will be the first end-to-end exercise.

🤖 Generated with Claude Code

The publish.yml workflow has been in the repo for a while but no
release has actually gone through it — all 6 PyPI versions
(0.0.1 through 0.1.3) were uploaded directly via twine from a
maintainer's laptop. No git tags exist on the remote, no GitHub
Releases page is populated, no changelog tracks what shipped when.

Three changes to fix the gap:

1. **publish.yml** — verify the tag matches `pyproject.toml`
   `[project].version` (fail fast on mismatch), then after the
   PyPI publish step run softprops/action-gh-release to create
   a GitHub Release with auto-generated notes and the built sdist
   + wheel attached. Adds the `contents: write` permission
   required for release creation.

2. **RELEASING.md** — explicit maintainer doc: bump version, bump
   changelog, commit, tag `vX.Y.Z`, push the tag, let CI do the
   rest. Calls out explicitly to NOT run `twine upload` locally
   anymore.

3. **CHANGELOG.md** — Keep-a-Changelog skeleton with an
   `[Unreleased]` section that already lists the substantive
   changes in flight (PRs #34, #38, #45, #47/#49, #48). The four
   pre-existing PyPI releases (0.1.0 / 0.1.1 / 0.1.3 — 0.1.2
   leaves no commit trail and only appears on PyPI) are backfilled
   with short summaries inferred from git log, honestly noting
   that no tag was created at the time.

Also locally cleaned up the stale `v0.1.0.dev0` tag that was sitting
in the working clone but never pushed.

This PR does not retroactively tag 0.1.3 because doing so would
trigger the workflow to attempt to re-publish 0.1.3, which PyPI
would reject. The first run of the new flow will happen the next
time a maintainer cuts a release.
@KylinMountain
Copy link
Copy Markdown
Collaborator Author

Code review

Found 2 issues:

  1. CHANGELOG [Unreleased] references the openkb init --language feature from PR feat(cli): add --language flag and prompt to openkb init #48, but feat(cli): add --language flag and prompt to openkb init #48 is OPEN, not merged — the feature does not exist at main HEAD. By convention [Unreleased] documents merged-but-not-released work; listing an open PR's feature misleads readers about what's actually in the codebase. Remove this bullet (re-add it when feat(cli): add --language flag and prompt to openkb init #48 merges):

OpenKB/CHANGELOG.md

Lines 13 to 18 in b82f786

matches (NFKC + case + `_``-`) get rewritten to canonical form,
unresolved targets become plain text. (#49)
- `openkb init --language` / `-l` flag and interactive prompt for wiki
output language at initialization. (#48)
- Anthropic prompt caching via `cache_control` markers in the compile
pipeline. Subsequent LLM calls reuse `(system + doc + summary +

  1. The [Unreleased] compare URL targets a tag that doesn't exist. git ls-remote --tags origin shows zero tags and this PR's own RELEASING.md (§ "Past releases not on git tags") acknowledges that 0.1.3 was published without tagging. The link 404s today. Either use compare/main...HEAD, or pin to the SHA 96cb6ef (the version-bump commit for 0.1.3), or drop the link until the first real tag exists:

OpenKB/CHANGELOG.md

Lines 74 to 76 in b82f786

[Unreleased]: https://github.com/VectifyAI/OpenKB/compare/v0.1.3...HEAD
[0.1.3]: https://pypi.org/project/openkb/0.1.3/

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…nto workflow

The Keep-a-Changelog file duplicates what `generate_release_notes: true`
already produces from merged PRs on the GitHub Releases page, with the
extra downside of needing manual maintenance — the initial review of
this PR caught three drift bugs in the backfilled CHANGELOG (an unmerged
PR mistakenly listed, a broken compare link, and a skipped 0.1.2). Drop
it; can be added back when the API stabilizes and a manual narrative
becomes useful.

RELEASING.md's value was its release flow + "don't run twine upload"
warning. Move both into a comment header at the top of publish.yml
itself, so the guidance lives next to the workflow it documents and is
visible to anyone reading the file. Removes a separate doc to maintain.

Net: PR now contains only the actual workflow change (tag/pyproject
version-match check + GitHub Release creation step + `contents: write`
permission for it).
v1.11.0 ships an older twine that only understands Metadata-Version up
to 2.3. Modern hatchling produces wheels with Metadata-Version 2.4, so
twine's pre-upload check fails with "InvalidDistribution: Metadata is
missing required fields: Name, Version" even though both fields are
present (twine just can't parse the newer metadata format).

Verified live: tag v0.1.4.dev0 reached the publish step and failed at
this check (run 25898978723). Bumping to v1.14.0 brings a recent twine
that handles 2.4 metadata.
… action

`pypa/gh-action-pypi-publish` is a Docker container action — it pulls
its image from `ghcr.io/pypa/gh-action-pypi-publish:<SHA>` keyed by
the commit SHA. The previous pin `6733eb7d741f0b11ec6a39b58540dab7590f9b7d`
was the SHA of the v1.14.0 annotated *tag object*, not the commit it
points to, so `ghcr.io/pypa/gh-action-pypi-publish:6733eb7d...`
doesn't exist and the docker pull fails with "manifest unknown".

The actual commit SHA for v1.14.0 is
cef221092ed1bacb1cc03d23a2d87d1d172e277b (resolved via
`git/tags/<tag-object-sha>` → `.object.sha`).

For JS actions (checkout, setup-python, softprops/action-gh-release)
this distinction doesn't matter because the action source is cloned by
git ref, which transparently dereferences tag objects. For Docker
container actions, the SHA is used literally as an image tag — so it
must be the commit SHA.

Verified live: run 25899049188 failed at this step with
`docker: Error response from daemon: manifest unknown` after the
v1.11.0→v1.14.0 bump in the prior commit. This commit corrects the
SHA so the image pull resolves.
Reverts the 0.1.4.dev0 bump from commit 2f9869d. The end-to-end dry-run
on the chore/release-process branch successfully published 0.1.4.dev0 to
PyPI via OIDC and created a GitHub Release (run 25899125441, all steps
green). PyPI's trusted publisher for openkb is confirmed correctly
configured.

Cleanup performed:
- Deleted v0.1.4.dev0 GitHub Release.
- Deleted v0.1.4.dev0 tag (local + remote).
- 0.1.4.dev0 on PyPI cannot be deleted (PyPI policy), but .dev0 suffix
  means pip will not install it by default — no user impact.

The branch can now be merged cleanly. The next real release will use
the same flow.
The previous flow required bumping pyproject.toml's `[project].version`
AND pushing a matching `vX.Y.Z` tag — two manual steps in sync, with a
workflow guard to catch drift. With hatch-vcs the version becomes a
single source of truth (the git tag), and the entire release reduces to
`git tag -a vX.Y.Z -m ... && git push origin vX.Y.Z`.

Changes:

- pyproject.toml: add `hatch-vcs` to build-system requires; mark version
  as `dynamic = ["version"]`; add `[tool.hatch.version] source = "vcs"`.
- publish.yml: drop the now-redundant tag/pyproject version-match step
  (the two sources are merged into one). Add `fetch-depth: 0` to the
  checkout so hatch-vcs sees the full tag history. Update the header
  comment to describe the simplified flow.

Local editable installs (`pip install -e .`) without a matching tag
will produce a dev-suffixed version like `0.1.3.post4.dev2+gabc1234`
("4 commits past v0.1.3, working tree at abc1234"). This is the
expected hatch-vcs behavior and doesn't affect tagged builds.
@KylinMountain KylinMountain merged commit b67f574 into main May 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant